# -*- coding: utf-8 -*-

from enum import IntEnum

from hlm.base import Pack
from utils.pyhelper import *

RULE_NAME = 'MARU_RULES'


class RuleItem(IntEnum):
    PackType = 0
    OpRule = 1
    AppendRule = 2
    ReturnRule = 3
    UserParam = 4
    DerivationParam = 5


RULE_LENGTH = max(list(RuleItem)) + 1


def convert_rule(mnemonic: str, op_pack, op_code_rule, op_append, op_return, user_param, der_param):
    # Check op pack type.
    if not issubclass(op_pack, Pack):
        raise Exception('The instruction structure of {} is not driven from Pack.'.format(mnemonic))
    # Construct an op pack here, extract all the fields it needs.
    test_pack = op_pack()
    op_fields = list(test_pack.__dict__.keys())
    # Process the op code.
    op_code_attr, op_code_value = op_code_rule
    py_set_opcode = 'op.{} = {}'.format(op_code_attr, op_code_value)
    op_fields.remove(op_code_attr)
    # Generate the user parameter definition.
    py_param = []
    py_key_param = []
    py_set_attr = []
    py_set_key_attr = []
    for rule in user_param:
        # Check the length of the rule.
        if len(rule) == 2:
            # Extract the rule.
            alias_name, attr_name = rule
            # Add the parameter.
            py_param.append(alias_name)
            # Set the attribute to pack.
            py_set_attr.append('op.{} = {}'.format(attr_name, alias_name))
        elif len(rule) == 3:
            # Extract the rule.
            alias_name, attr_name, default = rule
            # Append to parameter.
            py_key_param.append('{}=None'.format(alias_name))
            # Set the attribute to pack.
            py_set_key_attr.append('op.{} = ({}) if {} is None else ({})'.format(attr_name, default, alias_name, alias_name))
        else:
            raise Exception('Invalid user parameter rule detected in {}: {}'.format(mnemonic, rule))
        # Check validation.
        if attr_name not in op_fields:
            raise Exception('Failed to find {} in {}.'.format(attr_name, op_pack))
        op_fields.remove(attr_name)
    # Generate derivation rules.
    py_derivation = []
    for rule in der_param:
        if len(rule) != 2:
            raise Exception('Invalid user derivation rule detected in {}: {}'.format(mnemonic, rule))
        # Extract the parameter.
        attr_name, expression = rule
        # Check validation.
        if attr_name not in op_fields:
            raise Exception('Failed to find {} in {}.'.format(attr_name, op_pack))
        # Append the rules to derivation.
        py_derivation.append('op.{} = {}'.format(attr_name, expression))
        # Check validation.
        if attr_name not in op_fields:
            raise Exception('Failed to find {} in {}.'.format(attr_name, op_pack))
        op_fields.remove(attr_name)
    # Check the left op fields.
    if len(op_fields) > 1:
        raise Exception('Instruction {} is lacking rules for: {}'.format(mnemonic, ', '.join(op_fields)))
    func_def = 'def {}({}):'.format(mnemonic, ', '.join(py_param + py_key_param))
    # Add the op create method at the beginning of the body.
    func_body = ['op = {}()'.format(op_pack.__name__), py_set_opcode] + \
                py_set_attr + py_set_key_attr + py_derivation
    # Append the op to the target operation.
    if isinstance(op_append, str):
        op_append = "'{}'".format(op_append)
    # Generate the target list.
    target_list = 'OP_FILE[{}]'.format(op_append)
    func_body.append('{}.append(op)'.format(target_list))
    if op_return is not None:
        # Return the address.
        func_body.append('return len({}) - 1'.format(target_list))
    return func_def, func_body


def extract_parameter(user_param, rules, rule_index):
    while isinstance(user_param, str):
        user_param = rules[user_param][rule_index]
    return user_param


def convert_rule_file(filepath, includes, rules):
    # Construct the include lines.
    include_lines = []
    for package_name in includes:
        include_lines.append('from {} import *'.format(package_name))
    include_lines.append('from mtl.base import OP_FILE')
    include_lines.append('\n')
    # Prepare the file contents.
    file_lines = []
    # Convert rules.
    # Loop and convert the rules.
    for mnemonic in rules:
        rule = rules[mnemonic]
        # Extract the rule list.
        if len(rule) != RULE_LENGTH:
            raise Exception('Invalid rule format detected on {}.'.format(mnemonic))
        op_pack, op_code_rule, op_append, op_return, user_param, der_param = rule
        # Check the user and derivation parameter.
        user_param = extract_parameter(user_param, rules, RuleItem.UserParam)
        der_param = extract_parameter(der_param, rules, RuleItem.DerivationParam)
        # Convert the rules into Python code.
        func_def, func_body = convert_rule(mnemonic, op_pack, op_code_rule, op_append, op_return, user_param, der_param)
        # Append to rule file.
        file_lines.append(func_def)
        file_lines += [(PYTHON_INDENT + x) for x in func_body]
        file_lines.append('\n')
    # Open the file.
    with open(filepath, 'w') as rule_file:
        rule_file.write(PYTHON_FILE_HEADER)
        rule_file.write('\n')
        rule_file.write('\n'.join(include_lines))
        rule_file.write('\n')
        rule_file.write('\n'.join(file_lines))


def convert_rules(project_path: str, mtl_path: str):
    # Load the module from helper.
    rule_module = py_load_from_path(os.path.join(project_path, 'arch', 'rules.py'))
    # Extract the variables from module.
    if not hasattr(rule_module, RULE_NAME):
        raise Exception('Please define {} in the rules.py file.'.format(RULE_NAME))
    # Extract the rules.
    rules = getattr(rule_module, RULE_NAME)
    # Inside rule dictionary, it stores multiple files, generate each file based
    # on its parameter.
    for filename in rules:
        # Extract include and rules from result.
        includes, file_rules = rules[filename]
        # Construct the file path.
        filepath = os.path.join(mtl_path, '{}.py'.format(filename))
        # Convert the file.
        convert_rule_file(filepath, includes, file_rules)
